本篇內容我們將開始介紹 Dart 的一些基礎知識以及在使用 Flutter 時常見的語法或功能。畢竟,我們的目標是開發 Flutter 應用程式。
Dart 由 Google 主導於 2011 年公開。其開發團隊由 Lars Bak 和 Kasper Lund 領導,Lars Bak 是知名的虛擬機專家,曾領導開發 V8 JavaScript 引擎。
V8 的 8 表示作者 Lars Bak 寫的第 8 個虛擬機,之前的包含 Java、Smalltalk 等。
這個章節我們將涵蓋基礎語法、變數、型別、控制流程、函式等基礎。現今許多程式語言都會借鑑其他已存在語言的優點,Dart 也不例外,集合了許多高階成熟語言的功能。
在深入各種語法之前,先讓我們稍微認識一下這個語言,其中一個比較特別的是;Dart 可以在兩種環境下運行:
且執行 Dart 程式碼有兩種模式 JIT 和 AOT
所謂 Dart VM 你可以看成是一個用 C++ 開發彙整以下功能的一個程式
在 JIT 模式下,功能就是即時編譯器把程式碼轉換成機械碼,但是在 AOT 模式下會把用到的執行環境功能和程式碼一併打包編譯。因此,對於最終用戶來說,他們不需要在他們的作業系統上安裝任何關於 Dart 的執行環境或工具。
Dart 參考了許多已存在的語言因此開發者一般很容易從其他語言切換到 Dart,尤其是如果你有 C 或 JavaScript 的經驗,對於 Dart 的語法你應該不陌生。
Dart 使用 OO 的概念設計,也就是基於物件的概念。物件使用 class 定義,遵循了 OO 的原則,Dart 得益於封裝(Encapsulation),繼承(Inheritance),組合(Composition ),抽象化,多型多態性(Polymorphism)。
Dart 的型別系統不只讓開發者可以在編譯時期發現問題,而且也支援強大的自動補齊功能。除了一般的型別外,Dart 也支援 Null 安全。
不同於某些語言,Dart 也具備嚴謹的 Null 安全機制。空值(null)是程式語言中常見的概念 — 它簡單表示沒有值。與 JavaScript 不同,Dart 沒有未定義(undefined)的概念。Null 安全允許開發者指定和識別哪些變數或參數可以為空值,或哪些方法可以返回空值。有了這些識別,開發者可以根據需要調整其程式碼以應對可能的空值。
Dart 分析器還可以識別潛在的空指針異常(null pointer exceptions,即空值導致代碼失敗的情況),並迫使開發者透過額外檢查或通過緊縮類型來排除空值,從而準備應對這種情況。
type identifier = value;
通常我們開發時直接使用 var
讓編譯器自行推斷,如果無法推斷則為 dynamic
型別。
變數命名規則:
new
,class
。_
和 $
例外。這些和大部分語言差不多。
var inferredString = 'Hello';
String explicitString = "World";
Dart 的變數可以「沒有」值,稱為 null
,null
值時在 Dart 2.12 版本加入的,我們可以賦值 null
在 2.12 版本之前下面程式碼是正確的
int n; // n 初始化為 null
print(n); // null
n = 42;
print(n); // 輸出 42
在 2.12 之後的版本,上面寫法會顯示錯誤。如果要允許某個變數可以為 null
需要進一步處理,有 2 種方式:
?
宣告:int? n;
。late
宣告:late n;
。很多時候你知道某個變數在使用之前會設定值,但是在宣告的時候無法立刻用值初始化。在 Flutter 中的一個例子是,一個變數被宣告時無法被賦值,但在 Widget 初始化的時候會立即設定上去。這種情況就應該使用 late
型別。如同我們的猜測,如果一個變數可以是 null
那麼我們需要在使用之前檢查它是否為 null
,假如我們有一個變數負責計算儲存分數,且比賽開始之前為 null
:
int? goals;
print(goals + 2);
輸出 goals
變數把 null
+ 2 是會產生錯誤。為了解決這個問題,你可以顯式的檢查變數只有在變數不等於 null
才存取:
int? goals;
if (goals != null) {
print(goals + 2);
}
通過 if
陳述式,我們檢查了 goals
確定不等於 null
。Dart 會記得這個檢查並允許 + 2。
當 Dart 支援 Null 安全時,團隊決定改變語言的預設行為。團隊決定強制開發者重新評估他們的程式關於 Null 安全的重要性,而不是讓既有的程式繼續運作。雖然過程相當痛苦,很多 Bug 和程式需要改寫,但這讓 Flutter 生態圈變的更好。
Dart 為型別安全的程式語言,意味在撰寫程式和編譯時每個變數必須要定義型別。雖然型別是強制的,但型別註釋(Type annotations)不是。
這裡我們先提到 Type Inference v.s. Type Annotation。所謂型別註釋就只是明確宣告型別如 String str
而型別推斷則是讓編譯器幫我們分析推斷 var str
。
也就是說如果 Dart 可以推斷的話,我們就不需要明確的宣告型別。下面是一些內建的 Dart 資料型別:
num
,int
,double
bool
數字:
整數(int):64位有符號非小數整數值,範圍從 -2^63 到 2^63-1。例如 27、-1 和 534。
雙精度浮點數(double):Dart 使用 64 位雙精度浮點數來表示小數數值。例如 1.0、-57.00001 和 0.2。
兩者都是 num
型別,此外 Dart 還支援 dart:math
函式庫協助計算。
⚠️ 有些行為會根據平台有些不同。例如當執行在 Web 情況下 int 和 double 會編譯成 JavaScript 的 number ,那麼精度就只有 -2^53 到 2^53 -1
Dart 還支援 BigInt 型別其限制取決於系統的記憶體,但要小心使用,其效能不比 num
。
布林:
Dart 同樣也提供常見的 bool
型別 true
和 false
。布林是簡單的真值,用於邏輯處理。但不像 JavaScript
JavaScript 中 Falsy 有
false
,0
,-0
,空字串,null
,undefined
,NaN
除了這些其他都是值true
Dart 的布林型別相對嚴格,並且不採用跟 JavaScript 一樣的行為。
列表:
在 Dart 語言中,列表包含了其他語言中陣列(array)和列表(List)類型的功能。正如其名所示,列表存放一系列的值,其中這些值的順序是重要的。例如,一個有優先順序的活動列表或一個基於時間的事件列表可以存放在 Dart 的列表中。列表中的每個值都有一個索引(index)。
[index]
語法可用索引存取列表中的值+
可以串接兩個列表add
可以將值加入列表後面length
可以檢索長度remove
可以移除⚠️ 列表 List
預設並沒有長度限制。我們應使用 []
來建立列表,舊版使用 List
型別的建立方式已棄用:
List list = [];
print(list.length);
list.add('Hello');
List nums = [1, 2, 3];
建立列表時,可以設定一個長度來強制固定大小。固定大小的列表無法擴展:
List fixedList = List.filled(3, 'World');
fixedList.add('Hello'); // Error
在很多 OO 語言中會使用 new Type
的方式建立物件, Dart 過去也是這樣,但現在已不再使用 new 關鍵字來創建實例。但請注意,new
仍然是一個保留關鍵字,所以您不能將變量命名為 new
。
在 Dart 中 Map 是動態鍵值對集合,可以通過鍵來存取和編輯值。這和 List
非常類似,除了用索引來存取外,在 Map 是使用 Key。同時 Key 和 Value 可以是任何型別。跟 List
不同,Map 沒有排序的概念雖然其中一些 Map 型別可以執行排序。關於 Map 也提供一些方便的方法:
[key]
運算子來存取值length
可以取得 Map 長度remove
方法可以移除 Map 中的元素Map 應使用 {}
大括號來建立
Map nameAgeMap = {};
nameAgeMap['Alice'] = 23;
Map preFilledMap = {"Search": 1, "Alex": 2};
在 Dart,字串是一系列的字元 - UTF-16,主要是用來呈現文字。Dart 字串可以是單行或多行,搭配成對的單引號或雙引號來包住字元。
String a = 'Here is a signle quote string';
String b = "Here is a double quote string";
此外,多行字串可以使用 '''
或 """
建立(Python 也有類似的 f"""
)
String str = '''Here is a multi-line single
quote string''';
注意到第二排的縮排也會包含在字串中。
字串可以使用 +
號串接,此外 *
可以用來重複指定的數量,[index]
可以檢索字元。
String s1 = 'Here is a ';
String s2 = s1 + 'concatenated string';
print(s2[0]);
字串插值 String interpolation / variable expansion 是一個決定字串中佔位符 Placeholder 的動作,然後結合成結果。Dart 支援簡單的語法 ${}
。$
符號是標記佔位符要被計算的地方。如果要計算的位置是一個變數,那麼大括號可以省略,如果不能被省略那麼會出現警告。當一個佔位符涉及超過 1 個變數時,大括號就是用來註記邊界的。
String a = "Happy string";
print("The string is: $a"); // The string is: Happy string
print("The string length is ${$a.length}"); // The string length is 12
print("$a.length"); // 這樣會輸出 Happy string.length
Dart 也支援 Rune 來表示 UTF-32
字面量是一種標記方式,用來表示固定的值。我們在前面已經見過了
int
: 10, 1, -1, 5double
: 1.2bool
: true, falseString
: "Dart"List
: [1, 2, 3]Map
: {"a": 1, "b": 2}final
和 const
有時候我們希望變數的值可以固定,要達成這個需求我們可以使用 const
或 final
來確保值是固定的。
final String a = 'Staithes';
const int n = 3;
兩者有些許的差異,主要是這個值能否在編譯時期或執行時期被計算。如果一個變數在編譯時就可以決定其值,那麼應該使用 const
。如果其值需要在運行時才能確定,例如某個值需要在物件生命週期執行時才能賦值且只有一次,那就使用 final
as
有時候你會從沒有提供資料型別的來源取得資料。例如從 API 取得 JSON 資料。這種情況下資料會被註記為 dynamic
然後編譯器會允許你直接操作資料,因為它無法檢查資料。如同你的猜想,假如資料型別不是我們預期的那樣,會造成執行時期的錯誤。
舉例來說,如果你使用 dio
套件呼叫一個 API ,接著取得 Map<String, dynamic>
型別的回應和資料。其中 Key 是資料名稱,Value 這是動態型別的內容。
import 'package:dio/dio.dart';
void main() {
var dio = Dio();
try {
Response response = await dio.get('https://api.example.com/data');
Map<String, dynamic> data = response.data;
print(data['key']);
} catch (e) {
print(e);
}
}
如果你非常肯定 dynamic
值屬於某個型別,那麼你可以使用 as
關鍵字告訴編譯器。從這個時候開始,編譯器會認為它的型別是安全的,但這些全部源自於你假設你知道和註記了正確的型別。如果我們知道 JSON 回應的屬性 id
是字串我們可以如下
final id = json['id'] as String;
那麼 id
變數現在開始就是字串了,編譯器知道該如何檢查。然而如果結果證明 id
是可以為空的 String?
型別,那麼 as String
會在執行時期收到 null
時會失敗。現在我們概略了解型別安全、Null 安全、不可變(Immutable)等方式是如何運作了。
在 Dart,運算子只是「特殊語法的方法 Method」。當我們使用如 ==
運算子時,就像這樣執行方法 x.==(y)
比對 x 和 y 變數是否相同(這形式的方法設計和 Ruby 類似)。不像 Java 等語言具有原始基本型別的概念例如 int
、boolean
這些型別並不是物件,而是直接對應底層硬體的數據型別。上面的 x
是一個物件實例因此有自己的方法,也意味著運算子是可以被複寫,因此你可以在 class 中撰寫它們的邏輯。
後續我們會在函式和方法的章節深入介紹,因此你可能會回到這個段落重新閱讀。到此我們理解了為何運算子可以提供不同的功能,是取決於作用的型別。
Dart 同樣支援大多數程式語言常見的算術運算子
+ 加
- 減號和負數
* 乘號
/ 除
~/ 普通 / 除號得到的值會是 double。要單純只取得整數的部分,其他語言通常會需要其他操作;轉型。而 「~/」 只會返回整數的部分
% 取餘數
一些運算子根據左邊運算元的型別有不同的行為,例如 +
不只可以讓數字相加也可以串接字串。
同樣的,Dart 也支援計算縮寫:
int goals = 2;
goals += 3;
遞增 ++
和遞減 --
運算子
void main() {
var a = 5;
var b = a++; // 後置增量: 先賦值再遞增
print("a: $a, b: $b"); // a: 6, b: 5
var c = 5;
var d = ++c; // 前置增量: 先遞增再賦值
print("c: $c, d: $d"); // c: 6, d: 6
}
==
!=
>
<
>=
<=
Dart 只有 ==
運算子,Dart 是強型別語言,不會像 JavaScript 那樣進行自動類型轉換。例如,"1" == 1
在Dart 中會是 false
,而在 JavaScript 中會是 true
。對於基本資料型別 (如int, double, bool),==
比較的是值。物件類型預設是參考(Reference)。但比較特別的是 ==
是可以覆寫的,例如 String
類別重寫了 ==
運算子,比較的是字串的內容(值),而不是引用。
// 基本類型比較
print(1 == 1); // true
print(1 == 1.0); // true
print("1" == 1); // false
// 字符串比較
print("hello" == "hello"); // true, 比較的是值
// 列表比較
print([1, 2] == [1, 2]); // true, 比較的是內容
// 對象比較
class Person {
String name;
Person(this.name);
}
var p1 = Person("Alice");
var p2 = Person("Alice");
print(p1 == p2); // false, 比較的是引用
// 自定義 == 運算子
class Dog {
final String name;
Dog({required this.name});
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Dog &&
runtimeType == other.runtimeType &&
name == other.name;
@override
int get hashCode => name.hashCode;
}
Dart 的邏輯運算子應用在 bool
運算元。可以是變數,表達式或條件式。此外,可以結合複雜的表達式。
!expression
表達式結果的相反||
OR 邏輯&&
AND 邏輯上面我們快速的介紹了語法和其與其他語言運作的差異。同時,你可能也看到各種語言的身影如 Ruby、Python 等。在這個時間學習 Dart 確實不能算是非常容易,因為其已經經歷了幾次的重大轉變也加入了更多的功能。
早期的 Dart 確實更傾向於動態類型,隨著時間推移 2018 年引入了強型別系統,2019 年引入了 UI-as-code 概念,2.12 版本引入了 Null 安全特性,到了 Dart 3 更加入了 Pattern Matching 。這些功能都是基於社群的反饋和意見。總之,我們將循序漸進的掌握這些技能。另外,若想更即時的得知第一手的消息,我們也可以關注 Github dart-lang 和 flutter。